#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Purpose: Fields classes that can be associated to a Node
# Created: 01/22/2009
# $Id $
# Copyright (c) 2008,2009 by University College London
# Authors: 
# Didrik Pinte --<dpinte@dipole-consulting.com>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with HydroPlatform for details.
"""
TODO : 
 - harmonize how format attribute is used between the different fields 
(have only one declaration ...)

FIXME :
- using handlers could drastically simplify the dirty trick used to detect 
  updates
"""
import datetime
import logging
import numpy
import types
import time
import wx

from enthought.traits.api import HasTraits, Float, Unicode, Enum, Array, \
                                 List, ListInstance, Instance, Str, Date, \
                                 Property, Button, Trait, ListStr, Any, \
                                 cached_property, Bool, on_trait_change, \
                                 TraitError, Constant, Code, Int, DelegatesTo,\
                                 Either

from enthought.traits.ui.api import View, ArrayEditor, Item, HGroup, \
     ListEditor, TableEditor, TabularEditor, Handler, VGroup, Group, \
     EnumEditor, ListStrEditor, spring, DateEditor, CodeEditor
from enthought.traits.ui.table_column import TableColumn

from enthought.traits.ui.menu import OKButton, CancelButton
from enthought.traits.ui.key_bindings import KeyBinding, KeyBindings

# Chaco imports
from enthought.chaco.layers.api import ErrorLayer, WarningLayer, StatusLayer
from enthought.chaco.api import ArrayDataSource, ArrayPlotData, Plot, \
     ScatterInspectorOverlay, PlotAxis
from enthought.chaco.tools.api import PanTool, ZoomTool, RangeSelection, \
        RangeSelectionOverlay, LegendTool, ScatterInspector
from enthought.chaco.scales_tick_generator import ScalesTickGenerator
from enthought.chaco.scales_axis import PlotAxis as ScalesPlotAxis
from enthought.chaco.scales.api import CalendarScaleSystem
from enthought.chaco.scales.safetime import mktime

from hydroplatform.model.network import FieldValue
from hydroplatform.model import units


class Field(HasTraits):
    
    name = Str(label="Name")
    reference = Instance(FieldValue, transient=True)
    shared = Bool(False)
    units = Constant(units.loaded_units)
    
    _updated = Bool(False, transient=True)
    _configupdated = Bool(False, transient=True)
    
    @on_trait_change("_updated")    
    def refresh_reference(self, object, name, newvalue):
        if self.traits_inited() and newvalue is True:
            # refresh the reference data if any
            if self.reference is not None:
                self.reference.value = self.data   
                
    
    def _shared_changed(self):
        """
        When the shared attribute is changed, it means the configuration of 
        the Field has been updated
        """
        if self.traits_inited():
            self._configupdated = True
            
    
    def __eq__(self, obj):
        """
        Force implementation of __eq__ in subclasses so that
        sqlalchemy can correctly compare objects with the db instance and
        save them if needed
        """
        raise NotImplementedError()
            

class ObjectReference(Field):
    """
    This is a field refering to another node.
    
    It only has a name and a value
    
    FIXME : should allow filtering on a specific object type and not
            all the objects
    """
    format = Str("integer")
    single = Bool(False, label="Allow only one ref")
    class_filter = Str
    
    # default view for the parameter
    traits_view = View( HGroup(Item(name="name", style="readonly"), 
                               Item(name="shared"),
                               Item(name="class_filter"),
                               Item(name="single")),
                        buttons = [OKButton, CancelButton],
                        dock = "vertical"
    )
    
    def _data_changed(self):    
        if self.traits_inited():
            self._updated = True
            if self.reference is not None:
                self.reference.value = self.data    
    
    def __eq__(self, object):
        """
        Methods used by SQLAlchemy to compared the pickeled objects in the db
        
        Need to add a try:except block when the user switched from 
        SeasonalParameter to Parameter for example, then the data was not 
        anymore a scalar but an array !
        """
        try:
            if object is None:
                return False 
            if hasattr(object, 'data') and object.data != self.data:
                return False
            if hasattr(object, 'format') and object.format != self.format:
                return False
            if hasattr(object, 'name') and object.name != self.name:
                return False
            if hasattr(object, 'single') and object.single != self.single:
                return False
            return True
        except ValueError:
            return False
        
class NodeReference(ObjectReference):
    """
    This is a node refering to another node.
    
    It only has a name and a value
    """
 
    data = Int(label="Reference nodes")
    
class LinkReference(ObjectReference):
    """
    This is a field refering to another node.
    
    It only has a name and a value
    """
    data = Int(label="Reference links")
    
    
class NodeReferences(ObjectReference):
    """
    This is a field referring to a list of nodes.
    """
     
    data = List(label="Reference nodes")
    
class LinkReferences(ObjectReference):
    """
    This is a field referring to a list of links.
    """
    data = List(label="Reference links")    
    

class Parameter(Field):
    """
    A Parameter field stores a value with a given format and unit
    
    TODO : the data attribute should be creating using Any and updated based 
    on the format choosen by the user
    """
    
    unit = Str
    format = Enum("float", ["float", "integer", "binary"],
                label="Type",
                desc="provides information on the format of the data")
    data = Float(desc="Parameter value", \
                  label="Value")
    
    def __str__(self):
        return "%s - %s - shared %s - unit %s - format %s" % \
               (self.name, self.__class__.__name__, self.shared, self.unit, self.format)
     
    def _data_changed(self):    
        if self.traits_inited():
            self._updated = True
            if self.reference is not None:
                self.reference.value = self.data
    
    @on_trait_change("unit,format")
    def _format_changed(self):
        if self.traits_inited():
            self._configupdated = True
            return False
    
    # default view for the parameter
    traits_view = View( HGroup(Item(name="name", style="readonly"), 
                               Item(name="unit", 
                                    editor=EnumEditor(
                                        name="object.units.available_units")),
                               Item(name="format"),
                               Item(name="shared")),
                        buttons = [OKButton, CancelButton],
                        dock = "vertical"
    )
    
    def __eq__(self, object):
        """
        Methods used by SQLAlchemy to compared the pickeled objects in the db
        
        Need to add a try:except block when the user switched from 
        SeasonalParameter to Parameter for example, then the data was not 
        anymore a scalar but an array !
        """
        try:
            if object is None:
                return False 
            if hasattr(object, 'unit') and object.unit != self.unit:
                return False
            if hasattr(object, 'data') and object.data != self.data:
                return False
            if hasattr(object, 'format') and object.format != self.format:
                return False
            if hasattr(object, 'name') and object.name != self.name:
                return False
            return True
        except ValueError:
            return False
    
    
class TimeSerie(Field):
    # Time serie dimension is always two columns
    data = Array(shape=(None, 1))
    # read only values coming from the project 
    fromd = Date(label="Starting date")
    tod = Date(label="Ending date")    
    step = Enum(*units.TIMESTEPS.keys())
    
    tstep = Property(Str, label="Timestep", depends_on="step")
    dates = Property(depends_on="fromd,tod,step", transient=True)
    times = Property(depends_on="dates", transient=True)
    
    format = Enum("float", "integer", "binary",
                label="Type",
                desc="provides information on the format of the data")    
    
    unit = Str(desc = "Unit used by this parameter",\
                label = "Unit")
    
    paste_btn = Button(label="Paste", 
                       desc="Paste a table from the clipboard (e.g. copied from Excell/OpenOffice)",
                       transient=True)
    
    plotdata = ArrayPlotData(transient=True)
    ts_plot = Instance(Plot, transient=True)
    array_renderer = Any(transient=True)
    
    tskeybindings = KeyBindings(
        KeyBinding(binding1="Ctrl-v", 
                   description ="Paste",
                   method_name ='_ts_paste')
    )
    
    def __str__(self):
        tstep = units.TIMESTEPS[self.step].name
        return "%s - %s - shared %s - unit %s - format %s - " \
               "From %s to %s (step %s)" % \
               (self.name, self.__class__.__name__,  self.shared, self.unit, self.format, \
                self.fromd, self.tod, tstep)
    
    def _data_default(self):
        return numpy.empty((len(self.dates),1), dtype=numpy.float32) * numpy.NaN
    
    def _data_changed(self):    
        if self.traits_inited():
            # FIXME : check size and update accordingly
            self._updated = True
            d = self.reshape_data(self.data)
            if d is not None:
                self.data = d
            if self.reference is not None: 
                self.reference.value = self.data
                
    def reshape_data(self, data):
        """
        Reshape the data array according to the time horizon of the object
        
        Returns None if no need to update the data
        """
        if data is not None:
            logging.warn("Reshaping data")
            if not isinstance(data, numpy.ndarray):
                logging.debug("Data is not an ndarray ... : %s" % data)
                return numpy.empty((len(self.dates),1), numpy.float64) * numpy.NaN
            if data.shape[0] > len(self.dates):
                return data[:len(self.dates)]
            elif data.shape[0] < len(self.dates):
                enlarge_by = len(self.dates) - data.shape[0]
                # add some NaN to fill the array
                return numpy.concatenate((data,
                          numpy.empty((enlarge_by,1), data.dtype) * numpy.NaN))
            else:
                return None
        else:
            return None
                                         
    @on_trait_change("unit,format")
    def _format_changed(self):
        if self.traits_inited():
            self._configupdated = True            
            return False

    @on_trait_change("_updated")
    def refresh_plot(self, object=None, name=None, newvalue=True):
        if newvalue is False:
            return
        self.plotdata.set_data("values", self.data[:,0])
        if self.array_renderer is None and newvalue is True:
            self.array_renderer = self.ts_plot.plot(("times", "values"), type="line", name="TS", color="blue")
            if hasattr(self, 'status_overlay'):
                self.ts_plot.overlays.remove(self.status_overlay)
        self.ts_plot.request_redraw()
        
 
    def __eq__(self, object):
        try:
            if object is None:
                return False              
            if hasattr(object, "data") and  \
                numpy.all(object.data == self.data) is False:
                return False
            for att in ['fromd', 'tod', 'step', 'unit', 'name', 'format']:
                if getattr(object, att, None) != getattr(self, att):
                    return False
            else:
                return True
        except ValueError:
            return False
       
    @cached_property
    def _get_tstep(self):
        return units.TIMESTEPS[self.step].name
        
    @cached_property
    def _get_dates(self):
        if self.tod is None:
            return [1]
        return units.getDateSerie(self.fromd,  
                                self.tod, 
                                self.step) 
    @cached_property
    def _get_times(self):
       
        times  = []
        for d in self.dates:
            # mktime does not support dates < 1970 on windows CPython
            t = mktime(d.timetuple())
            times.append(t)
        #return numpy.array([time.mktime(d.timetuple()) for d in self.dates])
        return numpy.array(times)
    
    
    def _paste_btn_fired(self, value):
        self._ts_paste(value)

    def _ts_paste(self, val):
        '''
        Manage a paste event CTRL+V from the interface
        '''
        newarray = get_array_from_clipboard(1, self.data.dtype)
        if newarray is None:
            return False
        else:
            length = len(self.dates)
            newlength = newarray.shape[0]
            if newlength < length:
                # array is to small, increase its size
                self.data = \
                    numpy.concatenate((newarray, 
                                      self.data[newlength:, :]
                                      ))
            elif newlength > length:
                # array is too long, drop the unneeded elements
                self.data = newarray[0:length, :]
            else:
                self.data = newarray
            return True
        
    
    # default view for the parameter
    # need ETS 3.2.0 to get the readonly style on DateEditor
    traits_view = View( VGroup(Item(name="name",), 
                               Item(name="unit", style="simple",
                                    editor=EnumEditor(
                                        name="object.units.available_units")), 
                               Item(name="format",style="simple"),
                               Item(name="shared", style="simple"),
                               label="Parameters"
                               ),
                        VGroup(Item(name="fromd"),
                               Item(name="tod"),
                               Item(name="tstep"), 
                               style="readonly",
                               label="Time horizon"),
                        buttons = [OKButton, CancelButton],
                        style="readonly"
    )
    
    def _ts_plot_default(self):
        self.plotdata = ArrayPlotData(times = self.times)
        self.plotdata.set_data("values", self.data[:,0])
        plot = Plot(self.plotdata)
        plot.legend.visible = True
        plot.x_axis = None
        x_axis = ScalesPlotAxis(plot, orientation="bottom",
                        tick_generator=ScalesTickGenerator(scale=CalendarScaleSystem()))
        plot.overlays.append(x_axis)
        plot.x_grid.tick_generator = x_axis.tick_generator
        try:
            self.array_renderer = plot.plot(("times", "values"),
                                    type="line", name="TS", color="blue")
        except TraitError, e:
            self.status_overlay = WarningLayer(component=plot,
                                           align='ur', scale_factor=0.25)
            plot.overlays.append(self.status_overlay)
            self.array_renderer = None
        
        plot.tools.append(PanTool(plot, constrain=True, constrain_direction="x"))
        plot.overlays.append(ZoomTool(plot, tool_mode="range")) 
        return plot
    
    
    def update_time_horizon(self, project):
        """
        Update the time horizon of the TimeSerie using the project values
        """
        # call HasTraits.set in order to have the trait events updated
        self.set(fromd=project.horizon_from,
                 tod=project.horizon_to, 
                 step=project.time_step)
        
        # check array size
        if self.data.shape[0] > len(self.dates):
            self.data = self._data_default()
        elif self.data.shape[0] < len(self.dates):
            self.data = \
               numpy.concatenate((self.data, 
                                numpy.empty((len(self.dates) - self.data.shape[0], 1)) * numpy.NaN
                    ))
           

    
class TableCol(HasTraits):
    '''
    A TableCol is the definition of a column in a Table object
    
    It is a name and a unit
    '''
    name = Str
    unit = Str(desc = "Unit used in this column",\
               label = "Unit")
    units = Constant(units.loaded_units)
    format = Enum("float", "integer", "binary",
                label="Type",
                desc="provides information on the format of the data")    
 
    traits_view = View(HGroup(Item(name="name"), 
                              Item(name="unit",
                                editor=EnumEditor(
                                    name="object.units.available_units")),
                              Item(name="format")
                              ),
                       )
    
    
class Table(Field):
    '''
    Generic table to hold and expose data to a hydroplatform.lib.data.GenericTable
    '''    
    
    # FIXME : colNames and colUnits can be merged in a Dict of Str and Enum
    columns = List(Instance(TableCol, ()))
    data = Array(shape=(None, None))
    edit_columns = Button
    paste_btn = Button(label="Paste", 
        desc='''Paste a table from the clipboard \n
(e.g. copied from Excell/OpenOffice)''')

    colLabels = Property(depends_on="columns", transient=True)
    unit = Property(List, depends_on="columns")
    
    tablekeybindings = KeyBindings( KeyBinding(binding1="Ctrl-v", 
                                               description ="Paste",
                                               method_name ='_table_paste')
                                    )        
    
    traits_view =  View(HGroup(Item(name="name", style="readonly"),
                               Item(name="shared"),
                               Item(name="edit_columns")),
                        buttons = [OKButton, CancelButton],
                        dock = "vertical",
                        scrollable=True) 
    
    plotdata = ArrayPlotData(transient=True)
    xcol = Enum(values="colLabels", transient=True)
    ycol = Enum(values="colLabels", transient=True)
    array_plot = Instance(Plot, transient=True)
    array_renderer = Any(transient=True)
    has_plot = Bool(False)
    
    def __str__(self):
        description = "%s - %s - Columns : " % (self.name, self.__class__.__name__, ) 
        description += " | ".join(["Column(name='%s', unit='%s', format='%s')" % \
                                   (col.name, col.unit, col.format) \
                                   for col in self.columns])
        return description
        
    
    def _data_default(self):
        return numpy.empty((1, 1), 
                            dtype=numpy.float32) * numpy.NaN
#(len(self.columns) 
    def _data_changed(self):    
        if self.traits_inited():
            self._updated = True
          
            if self.reference is not None: 
                self.reference.value = self.data
            

    @on_trait_change("columns.name,columns.unit,columns_items,columns.format")
    def on_column_change(self):
        if self.traits_inited():
            self._configupdated = True
            return False
        
    def _get_unit(self):
        """
        Returns the list of units used in the table columns
        """
        units_list = list()
        for col in self.columns:
            units_list.append(col.unit)
        return units_list

    def _get_colLabels(self):
        return ["%s - [%s]" % (col.name, col.unit) for col in self.columns]
    
    def _edit_columns_fired(self, value):
        self.edit_traits(view=self.get_edit_col_view())
    
    def _paste_btn_fired(self, value):
        self._table_paste(value)
        self.array_plot.request_redraw()
    
    def get_edit_col_view(self):
        return View(
                        Item(name="columns",
                             editor = ListEditor(style="custom"),
                             style = "custom",
                            ),
                        buttons = [OKButton],
                        resizable=True,
                        width=0.5
                    )
    
    def __eq__(self, object):
        try:
            if object is None:
                return False
            if hasattr(object, 'data') and numpy.all(object.data == self.data) is False:
                return False
            for att in ['name', 'columns']:
                if getattr(object, att, None) != getattr(self, att):
                    return False
            else:
                return True
        except ValueError:
            return False
    
    def _table_paste(self, val):
        '''
        Manage a paste event CTRL+V from the interface
        
        FIXME : this should not exist in the model module but in the ui module !
        '''
        newarray = get_array_from_clipboard(len(self.columns), self.data.dtype)
        if newarray is None:
            return False
        else:
            self.data = newarray
            return True
        
    def _array_plot_default(self):
        
        plotdata = ArrayPlotData()        
        
        if len(self.columns) == 0:
            return None

        if self.data.shape[1] < len(self.columns):
            logging.warning('data has not enough columns ! Resizing it')
            self.data = numpy.resize(self.data, (self.data.shape[0], len(self.columns)))
        
        for idx in xrange(len(self.columns)):
            plotdata.set_data(str(idx), self.data[:, idx])
            
        self.plotdata = plotdata
        plot = Plot(self.plotdata, padding=0)
        plot.padding_left = 35
        plot.padding_bottom = 35

        try:
            self.array_renderer = plot.plot(("0", "1"), 
                                type="scatter", color="blue")[0]         
        except TraitError, e:
            logging.exception(e)
            self.status_overlay = WarningLayer(component=plot,
                                           align='ur', scale_factor=0.25)
            plot.overlays.append(self.status_overlay)
            self.array_renderer = None
       
        plot.tools.append(PanTool(plot, constrain=False))
        self.zoom_tool = ZoomTool(plot)
        plot.overlays.append(self.zoom_tool)
        
        self.xcol = self.colLabels[0]
        self.ycol = self.colLabels[1]
        self.has_plot = True
        return plot
        
    @on_trait_change("xcol,ycol")
    def _update_xy(self):
        
        if self.has_plot is False:
            return
        
        plot = self.array_plot

        xindex = str(self.colLabels.index(self.xcol))
        yindex = str(self.colLabels.index(self.ycol))
        
        if self.array_renderer is not None:
            plot.remove(self.array_renderer)
            
        try:
            self.array_renderer = plot.plot((xindex, yindex), 
                                type="scatter", color="blue")[0]
            value_range = plot.value_mapper.range
            index_range = plot.index_mapper.range
            index_range.low = numpy.min(self.data[:,int(xindex)])
            index_range.high = numpy.max(self.data[:,int(xindex)])
            value_range.low = numpy.min(self.data[:,int(yindex)])
            value_range.high = numpy.max(self.data[:,int(yindex)])
        except TraitError, e:
            self.status_overlay = WarningLayer(component=plot,
                                           align='ur', scale_factor=0.25)
            plot.overlays.append(self.status_overlay)
            self.array_renderer = None

        plot.request_redraw()
        
    @on_trait_change("_updated")
    def refresh_plot(self, object=None, name=None, newvalue=True):
        if newvalue is False or self.has_plot is False:
            return
        xindex = str(self.colLabels.index(self.xcol))
        yindex = str(self.colLabels.index(self.ycol))
            
        # refresh the datasets     
        for idx in xrange(len(self.columns)):
            self.plotdata.set_data(str(idx), self.data[:, idx]) 
        # try to plot if needed        
        if self.array_renderer is None and newvalue is True:
            self.array_plot.remove(self.array_renderer)
            self.array_renderer = self.array_plot.plot((xindex, yindex), 
                                type="scatter", color="blue")[0]        

            if hasattr(self, 'status_overlay'):
                self.array_plot.overlays.remove(self.status_overlay)
        else:
            # remove the old zoom_tool and then add it back to the plot
            # this fixes a problem of the ZoomTool not being updated correctly
            if self.zoom_tool is not None:
                self.array_plot.overlays.remove(self.zoom_tool)
            self.zoom_tool = ZoomTool(self.array_plot)
            self.array_plot.overlays.append(self.zoom_tool)
            # request redraw
            self.array_plot.request_redraw()
                        
def get_array_from_clipboard(column_limit, arraydtype):
        clipboard = wx.TextDataObject()
        if wx.TheClipboard.Open():
            wx.TheClipboard.GetData(clipboard)
            wx.TheClipboard.Close()
        else:
            wx.MessageBox("Can't open the clipboard", "Error")
        data = clipboard.GetText()
        # Format of data input using an OO copy
        # >>> data
        # u'100\t10\t1\n500\t50\t5\n1000\t100\t10\n2000\t200\t20\n5000\t500\t50\n'
        # Test implementation found here :
        # http://archives.devshed.com/forums/python-122/copy-and-pasting-selections-in-wx-grid-cells-2375883.html
        # Convert text in a array of lines
        if len(data) == 0:
            return None
        lines = data.splitlines()
        rows = len(lines)
        columns = len(lines[0].split("\t"))
        # check matrix dimension
        if columns < column_limit:
            raise FieldException("Cannot paste data without the needed columns")
        if columns > column_limit:
            logging.warning("More columns than needed, skipping the unneeded columns")
            columns = column_limit
        
        newarray = numpy.empty((rows, columns), dtype=arraydtype)
            
        for x, r in enumerate(data.splitlines()):                        
            c = r.split('\t')
            for y, val in enumerate(c):
                if y+1 > columns:
                    # skip unneeded columns
                    continue
                try:
                    float(val)
                    newarray[x,y] = val
                except Exception, e:
                    logging.error("Error while pasting data :%s - %s" % (val, e))
                    return None
                
        return newarray
        
class FieldException(Exception):
    pass


class SeasonalField(Field):
    '''
    This is the class to choose a seasonnality for a field
    
    
    The seasonality is defined by a period computed using :
    * a starting date
    * a fixed step (week, days, month) > the project timestep
    * an amount of step (integer value)
    '''
        
    # read only values coming from the project 
    fromd = Date(label="From")
    tod = Date(label="To")    
    step = Enum(*units.TIMESTEPS.keys())
    pstep = Property(label="Timestep", depends_on="step")
    
    # is the seasonnality on a year or the full horizon
    horizon = Bool(label="Horizon", 
                      desc="""if the time horizon is the year or the full perido
                      """) 
    # timestep
    starting_date = Date()
    sstep = Enum(None, units.TIMESTEPS.keys(), label="Step")
    tsteps = Property(Str, label="Period")
    _custom_step = Constant(units.CUSTOM)
    amount = Int(1, label="Amount") 
    step_button = Button(label="Edit custom step code")
    step_code = Code(label="Code")
                                 

    # computation date
    _fromDate = Property(depends_on="starting_date")
    _toDate = Property(depends_on="tod,horizon,starting_date")
    # generated sequence of dates and times based on the season
    dates = Property(label="Discretisation", depends_on="_dates", transient=True)
    _dates = Property(depends_on="_fromDate,_toDate,sstep,step_code,amount",
                     transient=True)
    _times = Property(depends_on="dates", transient=True) 
    
    
   
    
    def __init__(self, **kwargs):
        Field.__init__(self, **kwargs)
        self.starting_date = self.fromd
        self.sstep = self.step
        self._configupdated = False
        
    def __str__(self):
        return 
        
    def _fromd_changed(self):
        """
        When fromd is changed, check that starting_date is not before
        """
        if self.traits_inited is True:
            if self.starting_date is None or \
               self.fromd > self.starting_date:
                self.starting_date = self.fromd
        
    @on_trait_change('starting_date,sstep,horizon,amount,step_code', True)
    def _config_updated(self):
        if self.traits_inited():
            self._configupdated = True

    @cached_property
    def _get_tsteps(self):
        valid_steps = dict()
        for key, val in units.TIMESTEPS.items():
            if key >= self.step:
                valid_steps[key] = val.name
        return valid_steps
    
    @cached_property
    def _get_pstep(self):
        return units.TIMESTEPS[self.step].name
    
    @cached_property
    def _get__fromDate(self):
            return self.starting_date
        
    @cached_property
    def _get__toDate(self):         
        if self.horizon is True:
            if self.starting_date is None:
                return None
            # FIXME : need to substract one period from the computed date
            return datetime.date(self.starting_date.year +1, 
                                 self.starting_date.month,
                                 self.starting_date.day) - datetime.timedelta(days=1)
        else:
            return self.tod           
        
    def _step_button_fired(self):
        """
        Open a code editor
        """
        class CEditTraits(HasTraits):
            step_code = DelegatesTo('parent')
            parent = Instance(SeasonalParameter)
            
            traits_view = View(Item('step_code', 
                                    editor=CodeEditor(auto_set=False)),
                               buttons = [OKButton, CancelButton],
                               resizable=True)
            
        c = CEditTraits(parent=self)
        c.edit_traits()
    
    @cached_property
    def _get__dates(self):
        if self.sstep is None or \
           self._fromDate is None:
            return []
        if self.sstep is units.CUSTOM:
            try:
                exec(self.step_code)
            except Exception, e:
                logging.exception(e)
                return []
            if hasattr(self, 'result'):
                return self.result
            else: 
                return []
        else:
            step = units.TIMESTEPS[self.sstep].relativedelta.keys()[0]
            amount = self.amount * \
                   units.TIMESTEPS[self.sstep].relativedelta.values()[0]
            return units.getDateSerie(self._fromDate,  
                        self._toDate, 
                        0,
                        step,
                        amount)
 
    @cached_property
    def _get_dates(self):
        if self.horizon is True:            
            fmt = units.TIMESTEPS[self.sstep].shortformat
            if fmt is None:
                fmt = u"%Y-%m-%d"
        else:
            fmt = u"%Y-%m-%d"
            
        # FIXME : got errors due ot unicode/str conversion
        #Evgenii 13-04-11 added str to convert unicode fmt to string to fix FIXME
        return [d.strftime(str(fmt)) \
                for d in self._dates if d is not None]
    
    @cached_property
    def _get__times(self):
        if self._dates is None:
            return
        t = []
        for d in self._dates:
            # mktime does not support dates < 1970 on windows CPython
            t.append(mktime(d.timetuple()))
        #return numpy.array([time.mktime(d.timetuple()) for d in self._dates])
        return t
    
class SeasonalParameter(SeasonalField):
    """
    Parameter with a seasonality -> seld.data is an array with as many rows
    as the seasonnality discretisation
    """

    unit = Str
    format = Enum("float", "integer", "binary",
            label="Type",
            desc="provides information on the format of the data")    
  
    data = Array(shape=(None, None), label="Value")
    paste_btn = Button(label="Paste", 
        desc='''Paste a table from the clipboard \n
(e.g. copied from Excell/OpenOffice)''')
    spkeybindings = KeyBindings( KeyBinding(binding1="Ctrl-v", 
                                       description ="Paste",
                                       method_name ='_table_paste')
                            )                 
    
    def __init__(self, **kwargs):
        SeasonalField.__init__(self, **kwargs)    
    
    def __str__(self):        
        tstep = units.TIMESTEPS[self.sstep].name
        description =  "%s - %s - shared %s - unit %s - format %s - " \
               "From %s to %s (step %s, times %s)" % \
               (self.name, self.__class__.__name__,  self.shared, self.unit, self.format, \
                self.starting_date, self._toDate, tstep, self.amount)
        if self.sstep is units.CUSTOM:
            description += "\n<<<<<<<<\n%s\n<<<<<<<<" % self.step_code
        return description
    
    
    def _data_changed(self):    
        if self.traits_inited():
            self._updated = True
            updated_data =  self.reshape_data(self.data)
            if updated_data is not None:
                self.data = updated_data
            if self.reference is not None:
                self.reference.value = self.data
                
    def reshape_data(self, data):
        """
        Reshape the data array according to the time horizon of the object
        
        Returns None if no need to update the data
        """
        if data is not None :
            logging.warn("Reshaping data")
            logging.warn(data)
            logging.warn(self._dates)
            if data.shape[0] > len(self._dates):
                return data[:len(self._dates)]
            elif data.shape[0] < len(self._dates):
                enlarge_by = len(self._dates) - data.shape[0]
                logging.warn(enlarge_by)
                # create an 
                empty_dst = numpy.empty((enlarge_by,data.shape[1]), data.dtype) * numpy.NaN
                # add some NaN to fill the array                
                return numpy.concatenate((data, empty_dst))
            else:
                return None
        else:
            return None                
 
    def _table_paste(self, val):
        '''
        Manage a paste event CTRL+V from the interface
        
        FIXME : this should not exist in the model module but in the ui module !
        '''
        newarray = get_array_from_clipboard(self.data.shape[1], self.data.dtype)
        if newarray is None:
            return False
        else:
            self.data = newarray
            return True  
        
    def _paste_btn_fired(self, value):
        self._table_paste(value)
                
    @on_trait_change("unit,format")
    def _format_changed(self):
        if self.traits_inited():
            self._configupdated = True
            
    @on_trait_change('starting_date,sstep,horizon,amount,step_code')
    def _update_data(self):
        if self.traits_inited():
            print "updating the data size"
            self.data = self._data_default()
            
    def __eq__(self, object):
        try:
            if object is None:
                return False 
            if hasattr(object, 'data') \
               and numpy.all(object.data == self.data) is False:
                return False
            for att in ['unit', 'name', 'horizon', 'format', 'starting_date', \
                        'sstep', 'amount', 'step_code', 'fromd', 'tod', \
                        'step']:
                if getattr(object, att, None) != getattr(self, att):
                    return False
            else:
                return True
        except ValueError:
            return False
        return True    
    
    
    def _data_default(self):
        return numpy.empty((len(self._dates),1), dtype=numpy.float32) * numpy.NAN
        
    def update_time_horizon(self, project):
        """
        Update the time horizon of the SeasonalParameter using the project values
        """
        self.fromd = project.horizon_from
        if self.starting_date < self.fromd:
            self.starting_date = self.fromd
        self.tod = project.horizon_to
        self.step = project.time_step
        
        # call data changed to check the shape of data
        self._data_changed()
    
    traits_view = View(
                    Group(
                        Item(name="name", style="readonly"), 
                        Item(name="unit", editor=EnumEditor(
                                name="object.units.available_units")),
                        Item(name="format"),
                        Item(name="shared"),
                        label="General"), 
                    HGroup(
                             VGroup(
                                Group(Item("horizon",
                                           enabled_when="sstep != _custom_step"),
                                      Item("sstep", 
                                            editor=EnumEditor(name="tsteps")), 
                                      Item("starting_date", style="simple",
                                           editor=DateEditor(),
                                           enabled_when="sstep != _custom_step"),
                                      Item("amount",enabled_when="sstep != _custom_step"),
                                      Group(
                                          Item("step_button", 
                                                width      = 150,
                                                resizable  = False,
                                                show_label = True,
                                                springy=True
                                                ),
                                          spring,
                                          visible_when="sstep == _custom_step",
                                      ),
                                   label="Seasonal parameters"), 
                                 Group(Item("fromd"),
                                       Item("tod"),
                                       Item("pstep"),
                                       style="readonly",
                                       label="Project parameters")),
                             Group(
                                    Item("dates", 
                                      show_label=False,
                                      editor= ListStrEditor(editable=False),
                                      style="readonly",
                                      width=-100),
                                   spring,
                                   label="Discretisation"),
                                 spring,
                                label = "Seasonnality",
                             ),
                      resizable = True,
                      buttons = [OKButton, CancelButton],
                      dock = "vertical",
              )
